#include <string>
#include <unordered_map>
#include <vector>
#if defined (__linux__)
#   include <sys/inotify.h>
#elif defined (__APPLE__)
#   include <CoreServices/CoreServices.h>
#elif defined (_WIN32) || defined (_WIN64)
#   include <atlstr.h>
#endif
#include <easylog.h>
#include "util.hh"
#include "info.hh"

namespace cloud
{

class base_watcher
{
public:
    using callback_t = std::function<void(const std::string&)>;

    void regist(callback_t add_cb, callback_t mod_cb, callback_t del_cb)
    {
        _create_handler = add_cb;
        _modify_handler = mod_cb;
        _delete_handler = del_cb;
    }

    void check_cb()
    {
        if (!_create_handler) throw except("create handler is not registed");
        if (!_modify_handler) throw except("modify handler is not registed");
        if (!_delete_handler) throw except("delete handler is not registed");
    }

    base_watcher(const std::vector<std::string>& dirs = config::instance().back_dirs()) : _dirs(dirs)
    {}

    virtual ~base_watcher() {}

    virtual void watch() = 0;

protected:
    callback_t _create_handler;
    callback_t _modify_handler;
    callback_t _delete_handler;
    std::vector<std::string> _dirs;
};

#if defined (__APPLE__)

class watcher : public base_watcher
{
public:
    static watcher& instance()
    {
        static watcher inst;
        return inst;
    }

    ~watcher()
    {
        FSEventStreamStop(_sr);
        FSEventStreamInvalidate(_sr);
        FSEventStreamRelease(_sr);
    }

    virtual void watch() override
    {
        check_cb();

        CFStringRef tmp = CFStringCreateWithCString(nullptr, _dir.c_str(), kCFStringEncodingUTF8);

        FSEventStreamContext context{0, nullptr, nullptr, nullptr, nullptr};

        _sr = FSEventStreamCreate(NULL,
                &callback,
                &context,
                CFArrayCreate(nullptr, (const void**)&tmp, 1, nullptr),
                kFSEventStreamEventIdSinceNow,
                0.2,
                kFSEventStreamCreateFlagFileEvents);

        FSEventStreamScheduleWithRunLoop(_sr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

        if (!FSEventStreamStart(_sr))
            easylog::fatal("watcher model start failed");
        else
            easylog::info("watcher model started");

        CFRunLoopRun();
    }

private:
    static void callback(ConstFSEventStreamRef,
                                         void*,
                                        size_t num,
                                         void* epaths,
                const FSEventStreamEventFlags* eflags,
                   const FSEventStreamEventId*)
    {
        char **paths = (char**)epaths;

        for (int i = 0; i < num; ++i)
        {
            FSEventStreamEventFlags flags = eflags[i];

            if (eflags[i] & kFSEventStreamEventFlagItemIsFile)
            {
                std::string path(paths[i]);
                path = path.substr(path.rfind(config::instance().back_dir()));

                if (eflags[i] & kFSEventStreamEventFlagItemCreated)
                {
                    easylog::debug("file {} created", path);
                    watcher::instance()._create_handler(path);
                }
                else if (eflags[i] & kFSEventStreamEventFlagItemModified)
                {
                    easylog::debug("file {} modified",path);
                    watcher::instance()._modify_handler(path);
                }
                else if (eflags[i] & kFSEventStreamEventFlagItemRemoved)
                {
                    easylog::debug("file {} deleted", path);
                    watcher::instance()._delete_handler(path);
                }
                else
                {
                    easylog::error("other file action");
                }
            }
        }
    }

private:
    FSEventStreamRef _sr;
};

#elif defined (__linux__)

class watcher : public base_watcher
{
public:
    static watcher& instance()
    {
        static watcher inst;
        return inst;
    }

    virtual void watch()
    {
        check_cb();

        _infd = inotify_init();
        if (_infd < 0)
        {
            easylog::fatal("get detected directory handle failed");
            return;
        }

        for (auto& e : _dirs)
            add_watch(e);

        while (true)
        {
            loop_once();
        }
    }

private:
    void loop_once()
    {
        int size = 1024;
        std::unique_ptr<char[]> buf(new char[size]{0});

        ssize_t n = read(_infd, buf.get(), size);

        if (n <= 0 && errno != EINTR)
        {
            easylog::fatal("read directory change error");
            return;
        }

        struct inotify_event* event = nullptr;

        for (int i = 0; i < n; i += sizeof(struct inotify_event) + event->len)
        {
            event = (struct inotify_event*)&buf[i];

            std::string bpath = util::path::fpath2bpath(wd2path(event->wd), event->name);

            // TODO: 目录像文件一样对待
            if (event->mask & IN_ISDIR)
            {
                add_watch(bpath);
                continue;
            }

            if (event->mask & IN_CREATE)
            {
                easylog::debug("file {} created", bpath);
                _create_handler(bpath);
            }
            else if (event->mask & IN_MODIFY)
            {
                easylog::debug("file {} modified", bpath);
                _modify_handler(bpath);
            }
            else if (event->mask & IN_DELETE)
            {
                easylog::debug("file {} deleted", bpath);
                _delete_handler(bpath);
            }
            else
            {
                easylog::error("other file action");
            }
        }
    }

    void add_watch(const std::string& dir)
    {
        // wd stands for watch descriptor,
        // which can identify which monitor triggers the event.
        int wd = inotify_add_watch(_infd, dir.c_str(), IN_CREATE | IN_DELETE | IN_MODIFY);

        if (wd < 0)
        {
            easylog::error("add watch failed");
        }
        else
        {
            easylog::debug("add watch succ {}", dir);
            _wd2dir[wd] = dir;
        }
    }

    std::string wd2path(int wd)
    {
        if (_wd2dir.find(wd) == _wd2dir.end())
            throw except("get path by inwd error");

        return _wd2dir[wd];
    }

    int _infd;
    std::unordered_map<int, std::string> _wd2dir;
};

#elif defined (_WIN32) || defined (_WIN64)
class watcher : public base_watcher
{
public:
    static watcher& instance()
    {
        static watcher inst;
        return inst;
    }

    virtual void watch() override
    {
        check_cb();

        WCHAR wcdir[256];
        MultiByteToWideChar(CP_ACP, 0, _dirs[0].c_str(), _dirs[0].size() + 1, wcdir, 256);

        HANDLE hd = CreateFile(
            wcdir,
            FILE_LIST_DIRECTORY,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
            NULL
        );
        if (hd == INVALID_HANDLE_VALUE)
        {
            easylog::fatal("get detected directory handle failed");
            return;
        }

        while (true)
        {
            loop_once(wcdir, hd);
        }
    }

private:
    void loop_once(WCHAR* wcdir, HANDLE hdlr)
    {
        std::shared_ptr<BYTE> buf((LPBYTE)new char[4096]);
        PFILE_NOTIFY_INFORMATION pnotify = (PFILE_NOTIFY_INFORMATION)buf.get();
        DWORD  dw_buf_size;

        if (!ReadDirectoryChangesW(
                hdlr,
                buf.get(),
                4096,
                TRUE, // not only root dir
                // FILE_NOTIFY_CHANGE_LAST_WRITE
                FILE_NOTIFY_CHANGE_SIZE
                    | FILE_NOTIFY_CHANGE_DIR_NAME
                    | FILE_NOTIFY_CHANGE_FILE_NAME
                    | FILE_NOTIFY_CHANGE_CREATION,
                &dw_buf_size,
                NULL,
                NULL
            ))
        {
            easylog::fatal("read directory changesw error");
            return;
        }

        WCHAR sep[4] = {0};
        MultiByteToWideChar(CP_ACP, 0, "\\", 1 + 1, sep, 4);

        WCHAR path[MAX_PATH] = {0};
        memcpy(path,                     wcdir,             wcslen(wcdir) * sizeof(WCHAR));
        memcpy(path + wcslen(wcdir),     sep,               1 * sizeof(WCHAR));
        memcpy(path + wcslen(wcdir) + 1, pnotify->FileName, pnotify->FileNameLength);

        if (is_directory(path))
            return;

        if (pnotify->Action == FILE_ACTION_ADDED)
        {
            easylog::debug("file {} created", wchar2str(path));
            if (is_file_opened(path))
                return;
            _create_handler(wchar2str(path));
        }
        else if (pnotify->Action == FILE_ACTION_MODIFIED)
        {
            easylog::debug("file {} modified", wchar2str(path));
            if (is_file_opened(path))
                return;
            _modify_handler(wchar2str(path));
        }
        else if (pnotify->Action == FILE_ACTION_REMOVED)
        {
            easylog::debug("file {} removed", wchar2str(path));
            _delete_handler(wchar2str(path));
        }
        else
        {
            easylog::error("other file action");
        }
    }

private:
    std::string wchar2str(const LPCTSTR& widestr)
    {
        std::string tmp(
            WideCharToMultiByte(CP_OEMCP, NULL, widestr, -1, NULL, 0, NULL, FALSE),
            0);
        WideCharToMultiByte(CP_OEMCP, NULL, widestr, -1, &tmp[0], tmp.size(), NULL, FALSE);
        return tmp;
    }
    bool is_directory(const LPTSTR& path)
    {
        DWORD dwattr = GetFileAttributes(path);
        return static_cast<bool>((dwattr != 0xffffffff && (dwattr & FILE_ATTRIBUTE_DIRECTORY)));
    }
    bool is_file_opened(const std::wstring& path)
    {
        HANDLE hd = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
        if (hd != INVALID_HANDLE_VALUE)
        {
            CloseHandle(hd);
            return false;
        }
        return true;
    }
};
#endif
}
